{
 "cells": [
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {
    "tags": [
     "skip-execution"
    ]
   },
   "source": [
    "# 基于PyTorch训练和部署MNIST图片分类模型\n",
    "\n",
    "PyTorch是一个非常流行的深度学习框架，提供了极高的灵活性和优越的性能，能够与Python丰富的生态无缝结合，被广泛应用于图像分类、语音识别、自然语言处理、推荐、AIGC等领域。本示例中，我们将使用PAI Python SDK，在PAI完成一个PyTorch模型的训练，然后使用训练获得的模型部署推理服务。主要流程包括：\n",
    "\n",
    "- Step1: 安装和配置SDK\n",
    "\n",
    "安装PAI Python SDK，并配置使用的AccessKey、工作空间以及OSS Bucket。\n",
    "\n",
    "- Step2: 准备训练数据\n",
    "\n",
    "我们下载一个MNIST数据集，上传到OSS上供训练作业使用。\n",
    "\n",
    "- Step3: 准备训练脚本\n",
    "\n",
    "我们使用PyTorch示例仓库中的MNIST训练脚本作为模板，在简单修改之后作为训练脚本。\n",
    "\n",
    "- Step4: 提交训练作业\n",
    "\n",
    "使用PAI Python SDK提供的Estimator API，创建一个训练作业，提交到云上执行。\n",
    "\n",
    "- Step5: 部署推理服务\n",
    "\n",
    "将以上训练作业输出的模型，分别使用Processor和镜像部署的方式部署到PAI-EAS，创建在线推理服务。\n",
    "\n"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "## Step1: 安装和配置SDK\n",
    "\n",
    "我们需要首先安装PAI Python SDK以运行本示例。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "!python -m pip install --upgrade alipai\n",
    "!python -m pip install pandas"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\n",
    "\n",
    "SDK需要配置访问阿里云服务需要的AccessKey，以及当前使用的工作空间和OSS Bucket。在PAI SDK安装之后，通过在**命令行终端** 中执行以下命令，按照引导配置密钥、工作空间等信息。\n",
    "\n",
    "\n",
    "```shell\n",
    "\n",
    "# 以下命令，请在 命令行终端 中执行.\n",
    "\n",
    "python -m pai.toolkit.config\n",
    "\n",
    "```\n",
    "\n",
    "我们可以通过以下代码验证配置是否已生效。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "import pai\n",
    "from pai.session import get_default_session\n",
    "\n",
    "print(pai.__version__)\n",
    "\n",
    "sess = get_default_session()\n",
    "\n",
    "# 获取配置的工作空间信息\n",
    "assert sess.workspace_name is not None\n",
    "print(sess.workspace_name)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step2: 准备训练数据\n",
    "\n",
    "当前示例中，我们将使用MNIST数据集训练一个图片分类模型。为了支持训练作业加载使用，我们需要将数据上传到OSS Bucket上。\n"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "使用以下的Shell脚本，我们将MNIST数据集下载到本地目录data。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "shellscript"
    }
   },
   "outputs": [],
   "source": [
    "%%sh\n",
    "\n",
    "#!/bin/sh\n",
    "set -e\n",
    "\n",
    "url_prefix=\"https://ossci-datasets.s3.amazonaws.com/mnist/\"\n",
    "# 如果以上的地址下载速度较慢，可以使用以下地址\n",
    "# url_prefix=\"http://yann.lecun.com/exdb/mnist/\"\n",
    "\n",
    "mkdir -p data/MNIST/raw/\n",
    "\n",
    "wget ${url_prefix}train-images-idx3-ubyte.gz -P data/MNIST/raw/\n",
    "wget ${url_prefix}train-labels-idx1-ubyte.gz -P data/MNIST/raw\n",
    "wget ${url_prefix}t10k-images-idx3-ubyte.gz -P data/MNIST/raw\n",
    "wget ${url_prefix}t10k-labels-idx1-ubyte.gz -P data/MNIST/raw\n"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们将使用PAI Python SDK提供的OSS上传API，将相应的数据上传到OSS Bucket上。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from pai.common.oss_utils import upload\n",
    "from pai.session import get_default_session\n",
    "\n",
    "sess = get_default_session()\n",
    "data_path = \"./data\"\n",
    "\n",
    "data_uri = upload(data_path, oss_path=\"mnist/data/\", bucket=sess.oss_bucket)\n",
    "\n",
    "print(data_uri)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "## Step3: 准备训练脚本\n",
    "\n",
    "使用PyTorch训练模型，需要我们准备相应的脚本。这里我们以PyTorch官方提供的 [MNIST 示例](https://github.com/pytorch/examples/blob/main/mnist/main.py) 为基础，修改了数据加载和模型保存的逻辑，作为训练脚本。\n",
    "\n",
    "- 使用环境变量获得输入数据路径\n",
    "\n",
    "训练数据将被挂载到训练作业环境中使用，训练代码需要读取指定的路径获取训练数据。\n",
    "\n",
    "\n",
    "```diff\n",
    "\n",
    "-    dataset1 = datasets.MNIST(\"../data\", train=True, download=True, transform=transform)\n",
    "-    dataset2 = datasets.MNIST(\"../data\", train=False, transform=transform)\n",
    "\n",
    "+\t # 使用挂载到训练容器中的数据，默认为 /ml/input/{ChannelName}，可以通过环境变量 `PAI_INPUT_{ChannelNameUpperCase}`\n",
    "+    data_path = os.environ.get(\"PAI_INPUT_TRAIN_DATA\")\n",
    "+    dataset1 = datasets.MNIST(data_path, train=True, download=True, transform=transform)\n",
    "+    dataset2 = datasets.MNIST(data_path, train=False, transform=transform)\n",
    "\n",
    "\n",
    "```\n",
    "\n",
    "- 使用环境变量获取模型的保存路径：\n",
    "\n",
    "用户需要保存模型到工作容器中的指定路径，PAI的训练服务将其才能够持久化保存模型到OSS Bucket上。默认要求用户需要将模型保存到环境变量 `PAI_OUTPUT_MODEL` 指定的路径下（默认为`/ml/output/model`)。\n",
    "\n",
    "\n",
    "```diff\n",
    "\n",
    "-     if args.save_model:\n",
    "-         torch.save(model.state_dict(), \"mnist_cnn.pt\")\n",
    "\n",
    "+     # 保存模型\n",
    "+     save_model(model)\n",
    "+\n",
    "+\n",
    "+ def save_model(model):\n",
    "+     \"\"\"将模型转为TorchScript，保存到指定路径.\"\"\"\n",
    "\n",
    "+     output_model_path = os.environ.get(\"PAI_OUTPUT_MODEL\")\n",
    "+     os.makedirs(output_model_path, exist_ok=True)\n",
    "+\n",
    "+     m = torch.jit.script(model)\n",
    "+     m.save(os.path.join(output_model_path, \"mnist_cnn.pt\"))\n",
    "\n",
    "```\n",
    "\n",
    "PAI提供的预置[PyTorch Processor](https://help.aliyun.com/document_detail/470458.html) 在创建服务时，要求输入的模型是[TorchScript 格式](https://pytorch.org/docs/stable/jit.html) 。在本示例中，我们将模型导出为 `TorchScript格式` ，然后分别使用 `PyTorch Processor` 和镜像方式创建推理服务。\n"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "运行以下代码，创建一个训练脚本目录。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    },
    "tags": [
     "hide-cell"
    ]
   },
   "outputs": [],
   "source": [
    "!mkdir -p train_src"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "将训练作业脚本保存到`train_src`训练脚本目录，完整的作业脚本如下："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    },
    "tags": [
     "hide-cell"
    ]
   },
   "outputs": [],
   "source": [
    "%%writefile train_src/train.py\n",
    "\n",
    "# source: https://github.com/pytorch/examples/blob/main/mnist/main.py\n",
    "from __future__ import print_function\n",
    "\n",
    "import argparse\n",
    "import os\n",
    "\n",
    "import torch\n",
    "import torch.nn as nn\n",
    "import torch.nn.functional as F\n",
    "import torch.optim as optim\n",
    "from torch.optim.lr_scheduler import StepLR\n",
    "from torchvision import datasets, transforms\n",
    "\n",
    "\n",
    "class Net(nn.Module):\n",
    "    def __init__(self):\n",
    "        super(Net, self).__init__()\n",
    "        self.conv1 = nn.Conv2d(1, 32, 3, 1)\n",
    "        self.conv2 = nn.Conv2d(32, 64, 3, 1)\n",
    "        self.dropout1 = nn.Dropout(0.25)\n",
    "        self.dropout2 = nn.Dropout(0.5)\n",
    "        self.fc1 = nn.Linear(9216, 128)\n",
    "        self.fc2 = nn.Linear(128, 10)\n",
    "\n",
    "    def forward(self, x):\n",
    "        x = self.conv1(x)\n",
    "        x = F.relu(x)\n",
    "        x = self.conv2(x)\n",
    "        x = F.relu(x)\n",
    "        x = F.max_pool2d(x, 2)\n",
    "        x = self.dropout1(x)\n",
    "        x = torch.flatten(x, 1)\n",
    "        x = self.fc1(x)\n",
    "        x = F.relu(x)\n",
    "        x = self.dropout2(x)\n",
    "        x = self.fc2(x)\n",
    "        output = F.log_softmax(x, dim=1)\n",
    "        return output\n",
    "\n",
    "\n",
    "def train(args, model, device, train_loader, optimizer, epoch):\n",
    "    model.train()\n",
    "    for batch_idx, (data, target) in enumerate(train_loader):\n",
    "        data, target = data.to(device), target.to(device)\n",
    "        optimizer.zero_grad()\n",
    "        output = model(data)\n",
    "        loss = F.nll_loss(output, target)\n",
    "        loss.backward()\n",
    "        optimizer.step()\n",
    "        if batch_idx % args.log_interval == 0:\n",
    "            print(\n",
    "                \"Train Epoch: {} [{}/{} ({:.0f}%)]\\tLoss: {:.6f}\".format(\n",
    "                    epoch,\n",
    "                    batch_idx * len(data),\n",
    "                    len(train_loader.dataset),\n",
    "                    100.0 * batch_idx / len(train_loader),\n",
    "                    loss.item(),\n",
    "                )\n",
    "            )\n",
    "            if args.dry_run:\n",
    "                break\n",
    "\n",
    "\n",
    "def test(model, device, test_loader):\n",
    "    model.eval()\n",
    "    test_loss = 0\n",
    "    correct = 0\n",
    "    with torch.no_grad():\n",
    "        for data, target in test_loader:\n",
    "            data, target = data.to(device), target.to(device)\n",
    "            output = model(data)\n",
    "            test_loss += F.nll_loss(\n",
    "                output, target, reduction=\"sum\"\n",
    "            ).item()  # sum up batch loss\n",
    "            pred = output.argmax(\n",
    "                dim=1, keepdim=True\n",
    "            )  # get the index of the max log-probability\n",
    "            correct += pred.eq(target.view_as(pred)).sum().item()\n",
    "\n",
    "    test_loss /= len(test_loader.dataset)\n",
    "\n",
    "    print(\n",
    "        \"\\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\\n\".format(\n",
    "            test_loss,\n",
    "            correct,\n",
    "            len(test_loader.dataset),\n",
    "            100.0 * correct / len(test_loader.dataset),\n",
    "        )\n",
    "    )\n",
    "\n",
    "\n",
    "def main():\n",
    "    # Training settings\n",
    "    parser = argparse.ArgumentParser(description=\"PyTorch MNIST Example\")\n",
    "    parser.add_argument(\n",
    "        \"--batch-size\",\n",
    "        type=int,\n",
    "        default=64,\n",
    "        metavar=\"N\",\n",
    "        help=\"input batch size for training (default: 64)\",\n",
    "    )\n",
    "    parser.add_argument(\n",
    "        \"--test-batch-size\",\n",
    "        type=int,\n",
    "        default=1000,\n",
    "        metavar=\"N\",\n",
    "        help=\"input batch size for testing (default: 1000)\",\n",
    "    )\n",
    "    parser.add_argument(\n",
    "        \"--epochs\",\n",
    "        type=int,\n",
    "        default=14,\n",
    "        metavar=\"N\",\n",
    "        help=\"number of epochs to train (default: 14)\",\n",
    "    )\n",
    "    parser.add_argument(\n",
    "        \"--lr\",\n",
    "        type=float,\n",
    "        default=1.0,\n",
    "        metavar=\"LR\",\n",
    "        help=\"learning rate (default: 1.0)\",\n",
    "    )\n",
    "    parser.add_argument(\n",
    "        \"--gamma\",\n",
    "        type=float,\n",
    "        default=0.7,\n",
    "        metavar=\"M\",\n",
    "        help=\"Learning rate step gamma (default: 0.7)\",\n",
    "    )\n",
    "    parser.add_argument(\n",
    "        \"--no-cuda\", action=\"store_true\", default=False, help=\"disables CUDA training\"\n",
    "    )\n",
    "    parser.add_argument(\n",
    "        \"--dry-run\",\n",
    "        action=\"store_true\",\n",
    "        default=False,\n",
    "        help=\"quickly check a single pass\",\n",
    "    )\n",
    "    parser.add_argument(\n",
    "        \"--seed\", type=int, default=1, metavar=\"S\", help=\"random seed (default: 1)\"\n",
    "    )\n",
    "    parser.add_argument(\n",
    "        \"--log-interval\",\n",
    "        type=int,\n",
    "        default=10,\n",
    "        metavar=\"N\",\n",
    "        help=\"how many batches to wait before logging training status\",\n",
    "    )\n",
    "    parser.add_argument(\n",
    "        \"--save-model\",\n",
    "        action=\"store_true\",\n",
    "        default=False,\n",
    "        help=\"For Saving the current Model\",\n",
    "    )\n",
    "    args = parser.parse_args()\n",
    "    use_cuda = not args.no_cuda and torch.cuda.is_available()\n",
    "\n",
    "    torch.manual_seed(args.seed)\n",
    "\n",
    "    device = torch.device(\"cuda\" if use_cuda else \"cpu\")\n",
    "\n",
    "    train_kwargs = {\"batch_size\": args.batch_size}\n",
    "    test_kwargs = {\"batch_size\": args.test_batch_size}\n",
    "    if use_cuda:\n",
    "        cuda_kwargs = {\"num_workers\": 1, \"pin_memory\": True, \"shuffle\": True}\n",
    "        train_kwargs.update(cuda_kwargs)\n",
    "        test_kwargs.update(cuda_kwargs)\n",
    "\n",
    "    transform = transforms.Compose(\n",
    "        [transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,))]\n",
    "    )\n",
    "\n",
    "    data_path = os.environ.get(\"PAI_INPUT_TRAIN_DATA\", \"../data\")\n",
    "    dataset1 = datasets.MNIST(data_path, train=True, download=True, transform=transform)\n",
    "    dataset2 = datasets.MNIST(data_path, train=False, transform=transform)\n",
    "    train_loader = torch.utils.data.DataLoader(dataset1, **train_kwargs)\n",
    "    test_loader = torch.utils.data.DataLoader(dataset2, **test_kwargs)\n",
    "\n",
    "    model = Net().to(device)\n",
    "    optimizer = optim.Adadelta(model.parameters(), lr=args.lr)\n",
    "\n",
    "    scheduler = StepLR(optimizer, step_size=1, gamma=args.gamma)\n",
    "    for epoch in range(1, args.epochs + 1):\n",
    "        train(args, model, device, train_loader, optimizer, epoch)\n",
    "        test(model, device, test_loader)\n",
    "        scheduler.step()\n",
    "\n",
    "    # 保存模型\n",
    "    save_model(model)\n",
    "\n",
    "\n",
    "def save_model(model):\n",
    "    \"\"\"将模型转为TorchScript，保存到指定路径.\"\"\"\n",
    "    output_model_path = os.environ.get(\"PAI_OUTPUT_MODEL\", \"./model/\")\n",
    "    os.makedirs(output_model_path, exist_ok=True)\n",
    "\n",
    "    m = torch.jit.script(model)\n",
    "    m.save(os.path.join(output_model_path, \"mnist_cnn.pt\"))\n",
    "\n",
    "\n",
    "if __name__ == \"__main__\":\n",
    "    main()"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "## Step4: 提交训练作业\n",
    "\n",
    "`Estimator`支持用户使用本地的训练脚本，以指定的镜像在云上执行训练作业。通过`Estimator`，我们将以上准备的训练作业脚本提交到PAI，使用PAI提供的PyTorch镜像执行训练任务。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "from pai.estimator import Estimator\n",
    "from pai.image import retrieve\n",
    "\n",
    "\n",
    "# 使用PAI提供的PyTorch的GPU训练镜像\n",
    "image_uri = retrieve(\n",
    "    \"PyTorch\",\n",
    "    framework_version=\"1.8PAI\",\n",
    "    accelerator_type=\"GPU\",\n",
    ").image_uri\n",
    "\n",
    "print(image_uri)\n",
    "\n",
    "\n",
    "# 配置训练作业\n",
    "est = Estimator(\n",
    "    # 训练作业启动命令\n",
    "    command=\"python train.py --epochs 5 --batch-size 256 --lr 0.5\",\n",
    "    # 需要上传的代码文件\n",
    "    source_dir=\"./train_src/\",\n",
    "    # 训练作业镜像\n",
    "    image_uri=image_uri,\n",
    "    # 机器配置\n",
    "    # PAI的训练服务支持机器实例类型请见文档：[公共资源组实例和定价](https://help.aliyun.com/document_detail/171758.html?#section-55y-4tq-84y)\n",
    "    instance_type=\"ecs.gn6i-c4g1.xlarge\",  # 4vCPU 15GB 1*NVIDIA T4\n",
    "    # 训练作业的Metric捕获配置\n",
    "    # 训练服务支持从训练作业输出日志中（训练脚本打印的标准输出和标准错误输出），以正则表达式匹配的方式捕获训练作业Metrics信息。\n",
    "    metric_definitions=[\n",
    "        {\n",
    "            \"Name\": \"loss\",\n",
    "            \"Regex\": r\".*loss=([-+]?[0-9]*.?[0-9]+(?:[eE][-+]?[0-9]+)?).*\",\n",
    "        },\n",
    "    ],\n",
    "    base_job_name=\"pytorch_mnist\",\n",
    ")"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\n",
    "`estimator.fit`方法将用户的训练作业提交到PAI上执行。任务提交之后，SDK会打印作业详情页链接和训练作业的日志，等待作业执行结束。\n",
    "\n",
    "当用户需要直接使用OSS上数据，可以通过`estimator.fit`方法的`inputs`参数传递。通过`inputs`传递数据存储路径会被挂载到目录下，用户的训练脚本可以通过读取本地文件的方式加载数据。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 使用.fit方法提交训练作业\n",
    "est.fit(\n",
    "    inputs={\n",
    "        # 训练作业的输入数据，每一个Key，Value对是一个Channel，用户可以通过环境变量PAI_INPUT_{ChannelNameUpperCase}获取对应的数据路径\n",
    "        # 例如以下的train_data，训练的脚本中可以通过`PAI_INPUT_TRAIN_DATA`获取数据挂载后的路径.\n",
    "        \"train_data\": data_uri,\n",
    "    }\n",
    ")\n",
    "\n",
    "# 训练作业产出的模型路径\n",
    "print(\"TrainingJob output model data:\")\n",
    "print(est.model_data())"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "## Step5: 部署推理服务\n",
    "\n",
    "在训练作业结束之后，我们可以使用`estimator.model_data()`方法拿到训练作业产出模型的OSS路径。下面的流程中，我们将训练产出的模型部署到PAI创建在线推理服务。\n",
    "\n",
    "部署推理服务的主要流程包括：\n",
    "\n",
    "- 通过`InferenceSpec`描述如何使用模型构建推理服务\n",
    "\n",
    "用户可以选择使用Processor或是自定义镜像的模式进行模型部署。以下示例中将分别使用两种方式部署获得的PyTorch模型。\n",
    "\n",
    "- 通过`Model.deploy`方法，配置服务的使用资源，服务名称，等信息，创建推理服务。\n",
    "\n",
    "对于部署推理服务的详细介绍，可以见: [文档:部署推理服务](https://pai-sdk.oss-cn-shanghai.aliyuncs.com/pai/doc/latest/user-guide/model.html)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Processor 模式部署\n",
    "\n",
    "[Processor](https://help.aliyun.com/document_detail/111029.html) 是PAI对于推理服务程序包的抽象描述，他负责加载模型并启动模型推理服务。模型推理服务会暴露API支持用户进行调用。\n",
    "\n",
    "PAI提供了预置[PyTorch Processor](https://help.aliyun.com/document_detail/470458.html)，支持用户方便地将TorchScript格式的模型部署到PAI，创建推理服务。\n",
    "\n",
    "以下示例代码中，我们通过PyTorch Processor将训练产出的模型部署为一个推理服务。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    },
    "tags": []
   },
   "outputs": [],
   "source": [
    "from pai.model import Model, InferenceSpec\n",
    "from pai.predictor import Predictor\n",
    "from pai.common.utils import random_str\n",
    "\n",
    "\n",
    "m = Model(\n",
    "    model_data=est.model_data(),\n",
    "    # 使用PAI提供的PyTorch Processor\n",
    "    inference_spec=InferenceSpec(processor=\"pytorch_cpu_1.10\"),\n",
    ")\n",
    "\n",
    "p: Predictor = m.deploy(\n",
    "    service_name=\"tutorial_pt_mnist_proc_{}\".format(random_str(6)),\n",
    "    instance_type=\"ecs.c6.xlarge\",\n",
    ")\n",
    "\n",
    "print(p.service_name)\n",
    "print(p.service_status)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "`Model.deploy`返回的`Predictor`对象指向创建的推理服务，可以通过`Predictor.predict`方法发送预测请求给到服务，拿到预测结果。\n",
    "\n",
    "我们使用`numpy`构建了一个测试样本数据，发送给推理服务。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "\n",
    "# # 以上保存TorchScript模型要求输入为 Float32, 数据格式格式的形状为 (BatchSize, Channel, Height, Width)\n",
    "dummy_input = np.random.rand(2, 1, 28, 28).astype(np.float32)\n",
    "\n",
    "# np.random.rand(1, 1, 28, 28).dtype\n",
    "res = p.predict(dummy_input)\n",
    "print(res)\n",
    "\n",
    "print(np.argmax(res, 1))"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "在测试完成之后，可以通过`Predictor.delete_service`删除推理服务。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "p.delete_service()"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 镜像部署\n",
    "\n",
    "Processor模式启动的推理服务性能优越，适合于对于性能较为敏感的场景。对于一些需要灵活自定义的场景，例如模型使用了一些第三方的依赖，或是推理服务需要有前处理和后处理，用户可以通过镜像部署的方式实现。\n",
    "\n",
    "SDK提供了`pai.model.container_serving_spec()`方法，支持用户使用本地的推理服务代码配合PAI提供的基础镜像的方式创建推理服务。\n",
    "\n",
    "在使用镜像部署之前，我们需要准备模型服务的代码，负责加载模型、拉起HTTP Server、处理用户的推理请求。我们将使用Flask编写一个模型服务的代码，示例如下："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 准备推理代码保存目录\n",
    "!mkdir -p infer_src"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%writefile infer_src/run.py\n",
    "\n",
    "\n",
    "import json\n",
    "from flask import Flask, request\n",
    "from PIL import Image\n",
    "import os\n",
    "import torch\n",
    "import torchvision.transforms as transforms\n",
    "import numpy as np\n",
    "import io\n",
    "\n",
    "app = Flask(__name__)\n",
    "# 用户指定模型，默认会被加载到当前路径下。 \n",
    "MODEL_PATH = \"/eas/workspace/model/\"\n",
    "\n",
    "device = torch.device(\"cuda:0\" if torch.cuda.is_available() else \"cpu\")\n",
    "model = torch.jit.load(os.path.join(MODEL_PATH, \"mnist_cnn.pt\"), map_location=device).to(device)\n",
    "transform = transforms.Compose(\n",
    "    [transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,))]\n",
    ")\n",
    "\n",
    "\n",
    "@app.route(\"/\", methods=[\"POST\"])\n",
    "def predict():\n",
    "    # 预处理图片数据\n",
    "    im = Image.open(io.BytesIO(request.data))\n",
    "    input_tensor = transform(im).to(device)\n",
    "    input_tensor.unsqueeze_(0)\n",
    "    # 使用模型进行推理\n",
    "    output_tensor = model(input_tensor)\n",
    "    pred_res =output_tensor.detach().cpu().numpy()[0] \n",
    "\n",
    "    return json.dumps(pred_res.tolist())\n",
    "\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    app.run(host=\"0.0.0.0\", port=int(os.environ.get(\"LISTENING_PORT\", 8000)))\n"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\n",
    "\n",
    "通过`pai.model.container_serving_spec`，我们基于本地脚本和PAI提供的`PyTorch`镜像创建了一个`InferenceSpec`对象。\n",
    "\n",
    "- 模型服务的代码和启动命令：\n",
    "  \n",
    "用户指定的本地脚本目录source_dir会被上传到OSS，然后挂载到服务容器（默认到 /ml/usercode目录）。\n",
    "\n",
    "- 推理服务镜像：\n",
    "\n",
    "PAI 提供了基础的推理镜像支持用户使用，用户可以通过`pai.image.retrieve`方法，指定参数`image_scope=ImageScope.INFERENCE`获取PAI提供的推理镜像。\n",
    "\n",
    "- 模型服务的第三方依赖包：\n",
    "\n",
    "模型服务代码或是模型的依赖，可以通过`requirements`参数指定，相应的依赖会在服务程序启动前被安装到环境中。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from pai.model import InferenceSpec, container_serving_spec\n",
    "from pai.image import retrieve, ImageScope\n",
    "\n",
    "torch_image_uri = retrieve(\n",
    "    framework_name=\"pytorch\", framework_version=\"1.12\", accelerator_type=\"CPU\"\n",
    ").image_uri\n",
    "\n",
    "inf_spec = container_serving_spec(\n",
    "    command=\"python run.py\",\n",
    "    source_dir=\"./infer_src/\",\n",
    "    image_uri=torch_image_uri,\n",
    "    requirements=[\"flask==2.0.0\"],\n",
    ")\n",
    "print(inf_spec.to_dict())"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "使用训练作业输出的模型，以及以上的 InferenceSpec，我们将通过 Model.deploy API部署一个在线推理服务。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from pai.model import Model\n",
    "from pai.common.utils import random_str\n",
    "import numpy as np\n",
    "\n",
    "\n",
    "m = Model(\n",
    "    model_data=est.model_data(),\n",
    "    inference_spec=inf_spec,\n",
    ")\n",
    "\n",
    "predictor = m.deploy(\n",
    "    service_name=\"torch_mnist_script_container_{}\".format(random_str(6)),\n",
    "    instance_type=\"ecs.c6.xlarge\",\n",
    ")"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\n",
    "我们准备一张 MNIST 测试图片，用于发送给到推理服务。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    },
    "tags": [
     "keep_output"
    ]
   },
   "outputs": [
    {
     "data": {
      "image/png": "iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAAAAABXZoBIAAABvklEQVR4nF2SO2tUURSFv73PPufMMzGG+EALhZhoaxBs7EWwCoiNhfgbLERrQSwshfwA7SyTIIJgJVgpBBEhEZuQQsIgE/XeubMtZu4wk9UuztrrcWAMEwACGQQSs9CUFARyApuhogKICSFgzZFaTZaoigwHolSo2GD6pYTRUTOoZbUmvXJodhoDMUJDwqyflAGYA6vNyrSntfXbq38e95a3PwQtAVSwjELm/v6hb30ZlL6Rwlg2gSLQvb7vn9bS3JuhP6EzFmsIEoF77ptB853Sd05oitRJrcXp5/5zY9Gwb95fR8dlKiCZh799K2Hdm4e7T1s6ZTIiZ/aq11hc2nF/Hw0h1wEj4awX50892vS/v/q3YIl5JrK0WgfeL32wu+c/WJyZKsLcaq/4+uziuXfFi0QkyuhsAIWwQIb5G+4PkPZk6gQSgNCE7mXvrUAgjQcx0Fh7Pzn0DgJGBLAKvHINPgj56GpRNigKnBLAXHDHpaI6YjkXw3/lZCrFAajoBvhcdFMJUk11FEzHE3/3a22yYFPsKK0m7vrHS3Qjx2EE48rLg7dEYvsYqSKCXnjVX2lrg9kPJpOiAVnIAP8B0Kx+GvoyGWQAAAAASUVORK5CYII=",
      "text/plain": [
       "<PIL.JpegImagePlugin.JpegImageFile image mode=L size=28x28 at 0x7FB14BAC02E0>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "\n",
    "!pip install -q pillow\n",
    "\n",
    "\n",
    "import base64\n",
    "from PIL import Image\n",
    "from IPython import display\n",
    "import io\n",
    "\n",
    "\n",
    "# raw_data是一张MNIST图片，对应数字9\n",
    "raw_data = base64.b64decode(b\"/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/wAALCAAcABwBAREA/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/9oACAEBAAA/APn+rVhpmoarP5GnWNzeTYz5dvE0jfkoJovNMv8ATmK3tjc2zByhE8TIQw6jkdR6VVq9oumPrWuWGlxyLG95cRwK7dFLMFyfzr3aXwp4ltAfB3gWwudI01JNuoa7eZhku5AMHafvFOw2Dn6ZJ4z4yeLk1HUbXwrZSSy2Oh5heeaQu88wG1mLHk4wR9c+1eXUqsVYMpIIOQR2r1D4QazqOs/FnSG1fVLi9ZI5vL+2TNKc+U2ApYnB7/hXml5LLNfXEsxLSvIzOSMEsTk1DRVnT7+60vULe/spmhureQSRSL1Vh0NWNd1mXX9ZuNUuLe2gmuCGkS2QohbABbBJwTjJ9yelZ1f/2Q==\")\n",
    "\n",
    "im = Image.open(io.BytesIO(raw_data))\n",
    "\n",
    "display.display(im)\n"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "推理服务使用 HTTP 请求体内的数据作为输入的图片，SDK 的 `raw_predict` 方法接受 bytes 数据类型的请求，通过 POST 方法，在请求内带上用户推理数据，发送给到推理服务。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import json\n",
    "from pai.predictor import RawResponse\n",
    "\n",
    "resp: RawResponse = predictor.raw_predict(data=raw_data)\n",
    "print(resp.json())\n",
    "\n",
    "print(np.argmax(resp.json()))"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "测试完成之后可以删除服务。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "predictor.delete_service()"
   ]
  }
 ],
 "metadata": {
  "execution": {
   "timeout": 1800
  },
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.16"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 0
}
